STM32(Cortex

您所在的位置:网站首页 stm32 busfault_handler STM32(Cortex

STM32(Cortex

2023-04-18 15:57| 来源: 网络整理| 查看: 265

启动模式

stm32有多种启动模式,以stm32f4xx为例,如下图所示:

最常见的是第一种,从片上flash启动,也是芯片的正常运行模式。

第二种从system memory启动,仅适用于使用串口下载程序或者使用USB-DFU模式下载程序的情况,程序同样是下载到flash。

第三种从SRAM启动一般用于程序调试的情况,使用也较少。

(并非所有芯片都适用于这个表格,不同型号具体启动方式请参考对应手册,st官方也有统一的bootloader介绍文档可供查阅)

启动文件

我们先来分析一下stm32的启动文件,即startup_stm32xxxxxx.s(只要是Cortex-M内核的芯片都会有这个启动文件,名称可能有所区别)。

Stack_Size EQU 0x400 ​ AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp

分配了一段大小为1KB的栈空间,段名STACK,可读写,ALIGN=3表示2^3=8字节对齐,__initial_sp紧挨着栈的结束地址,由于栈是从高往低生长,所以__initial_sp的位置就是栈顶。

Heap_Size EQU 0x200 ​ AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit

分配了一段大小为512字节的堆空间,段名HEAP,可读可写,8字节对齐,__heap_base和__heap_limit分别是堆的起始地址和结束地址。

PRESERVE8 THUMB

指定当前文件的堆栈按照8字节对齐,后面指令兼容16位的Thumb指令。Cortex-M内核实际使用的是Thumb-2指令集,将16位与32位指令混合使用。

AREA RESET, DATA, READONLY EXPORT __Vectors EXPORT __Vectors_End EXPORT __Vectors_Size ​ __Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler DCD HardFault_Handler ; Hard Fault Handler DCD MemManage_Handler ; MPU Fault Handler DCD BusFault_Handler ; Bus Fault Handler DCD UsageFault_Handler ; Usage Fault Handler DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD SVC_Handler ; SVCall Handler DCD DebugMon_Handler ; Debug Monitor Handler DCD 0 ; Reserved DCD PendSV_Handler ; PendSV Handler DCD SysTick_Handler ; SysTick Handler ​ ; External Interrupts DCD WWDG_IRQHandler ; Window WatchDog DCD PVD_IRQHandler ; PVD through EXTI Line detection ………… DCD 0 ; Reserved DCD SPI4_IRQHandler ; SPI4 ​ __Vectors_End ​ __Vectors_Size EQU __Vectors_End - __Vectors ​

定义了一个数据段,名为DATA,仅可读。

上文为堆栈分配的空间均位于SRAM中,不占用代码空间,从这个数据段开始才是stm32代码空间的起始位置,先定义并初始化了栈顶位置(__initial_sp)以及15个内核异常处理函数的入口地址,接下来是外部中断,最后用结束地址减去开始地址得到__Vectors_Size即本数据段的大小。

Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main ​ LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP

reset_handler即复位程序的实际执行代码,上电或是复位都会先从这里开始执行然后进入main函数,具体的执行过程暂且按下不表,我们继续看启动文件的后续内容。

NMI_Handler PROC EXPORT NMI_Handler [WEAK] B . ENDP HardFault_Handler\ PROC EXPORT HardFault_Handler [WEAK] B . ENDP ………… ​ SysTick_Handler PROC EXPORT SysTick_Handler [WEAK] B . ENDP ​ Default_Handler PROC ​ EXPORT WWDG_IRQHandler [WEAK] EXPORT PVD_IRQHandler [WEAK] EXPORT TAMP_STAMP_IRQHandler [WEAK] ………… EXPORT SPI4_IRQHandler [WEAK] WWDG_IRQHandler PVD_IRQHandler TAMP_STAMP_IRQHandler ………… SPI4_IRQHandler B . ​ ENDP ​ ALIGN

将除了reset_handler外的内核异常都分别写成无限循环(B .)的弱函数,外部中断也是如此,只是函数起始位置都是同一个地址。

IF :DEF:__MICROLIB EXPORT __initial_sp EXPORT __heap_base EXPORT __heap_limit ELSE IMPORT __use_two_region_memory EXPORT __user_initial_stackheap __user_initial_stackheap ​ LDR R0, = Heap_Mem LDR R1, =(Stack_Mem + Stack_Size) LDR R2, = (Heap_Mem + Heap_Size) LDR R3, = Stack_Mem BX LR ​ ALIGN ​ ENDIF ​ END

如果使用了microlib则将栈顶地址和堆的起始、结束地址export出去,microlib会进行堆栈初始化的操作,若是没有使用,则堆栈初始化时会使用__user_initial_stackheap函数。

启动流程

stm32的代码是烧写到flash中的,通过查询手册可知,flash的起始地址是0x08000000:

通过keil已配置好工程的flash download界面也可以查看烧写位置和大小。

但是Cortex-M内核规定上电后必须从0x00000000的位置开始执行,这就需要一个地址映射的操作,不论stm32的启动模式是本文开头说的哪一种,都会将该启动区域的代码映射到0x00000000的位置,进入keil的调试模式打开memory窗口:

可以看到,0x00000000开始的数据和flash起始位置0x08000000开始的数据是完全相同的。

分析启动代码时我们提到过,从DATA段开始才是代码段的起始位置,那么0x08000000作为起始地址的4字节空间存储的就是__initial_sp的地址,即栈顶地址,上电或复位后,硬件会自动将该地址赋给msp,即主栈指针,随后将0x08000004作为起始地址的4字节空间内容,也就是reset_handler函数入口地址赋给PC。

此时程序会立刻去执行reset_handler,让我们回过头来看看reset_handler中做了些什么:

Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main ​ LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP

可以看到,reset_handler中执行了两个程序,systeminit和__main。

其中不同型号芯片的默认systeminit函数有所区别,内容往往是初始化时钟、FPU等。

用户没有重写__main的情况下,该函数并不在库文件中,而是由armlink创建,如果想要在keil调试的时候查看__main的具体执行过程,需要在BX R0指令运行前点击汇编代码的内容框,再继续单步执行,则可以看到__main的汇编代码,否则会直接跳进main函数。

通过查询官方文档可以得知,__main中调用了__scatterload和__rt_entry两个函数:

补充一下官方文档的说明:应用代码和数据可能存于“根区域”或是“非根区域”,前者拥有相同的加载和执行地址,后者则不同。(这里结合文档,我的个人理解是:只读的代码和变量这类不需要移动的数据都属于“根区域”,即flash中,而可读可写的数据属于“非根区域”,因为要搬运到RAM中才能实现写操作,也不排除用户自行定义变量存储位置时,需要额外的搬运工作,这也属于“非根区域”)

默认的__scatterload函数做了两件事:

1.将ZI段数据全部初始化为0

2.将“非根区域”的数据从加载域复制到执行域(将可读可写的数据搬运到RAM)

在keil工程目录下的${工程名}.sct文件中可以查看加载域和执行域的地址以及数据存储位置:

; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* ​ LR_IROM1 0x08000000 0x00080000 { ; load region size_region ER_IROM1 0x08000000 0x00080000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x00018000 { ; RW data .ANY (+RW +ZI) } }

可以看到,加载域的地址就是flash的地址,且只读的数据(RO、XO)放在flash(0x08000000--0x08080000)中,可读可写的数据(RW、ZI)放在RAM(0x20000000--0x20018000)中。

__scatterload函数执行完后接着调用__rt_entry,__rt_entry又调用了如下函数:

其中__user_setup_stackheap就是启动文件末尾的函数,如果没有使用microlib的话就会调用它。

__rt_entry负责初始化堆栈以及C语言库子系统,即C语言代码运行所必需的环境,随后万事俱备,就可以跳入main函数执行C语言代码了。

当然,如果没有在main函数中写死循环的话,main函数执行完了后会到达exit退出程序。

总结

简单总结一下,芯片上电第一件事是把代码空间的第一行,即栈顶指针赋给msp,随后将第二行reset_handler的入口地址赋给pc,使程序立刻跳转去执行reset_handler,在reset_handler中先初始化时钟、FPU等硬件相关配置(具体内容根据芯片型号有所不同),再初始化ZI段,将代码和数据从加载域搬运到执行域,随后初始化堆栈和C语言库子系统,使得C语言代码能够正常运行,最后跳入main函数执行用户编写的C程序。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3